iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0
Mobile Development

Flutter 開發實戰 - 30 天逃離新手村系列 第 30

Day 30 發佈正式版應用程式

  • 分享至 

  • xImage
  •  

當我們撰寫完程式碼,也跑過測試,處理完所有問題,一切準備就緒。是時候將應用程式發佈到平台上了。

本章節將探討如何在 Apple App Store、Google Play Store 上架。每個平台的上架都有其挑戰,這個章節將作為一個引導,說明在發佈過程中的一些關鍵步驟,當然雖然時間推移,上架的步驟或多或少可能有些改變,你可能還是需要參考官方文件的說明。

到了這一步,你可能會覺得我們已經在模擬器和裝置成功運行了,因此應該就完成了吧。從 Flutter 的角度來說我們確實概念上準備完成了,但還有一部分需要說明。

讓我們先回顧一下 Flutter 的幾個目標:

  • 縮短開發時間
  • 提供足夠優秀的測試工具

一個應用程式框架可以滿足這些功能,但也可能產生出非常慢的應用程式,這是因為為了達成這些目標必須使用 JIT 模式以達成程式碼可以即時變更,且還須和外部分享這些資訊以方便進行測試等等。

而另一方面 Flutter 的另一個目標 - 是產生接近原生高效能的應用程式,而這就是為什麼 Dart 支援兩種編譯方式的原因。

在準備發佈應用程式的階段,顯然 JIT 不再合適,此時比較好的作法是採用 AOT 提供一個優化過,高效能的應用程式。在預先編譯 AOT 模式下,一些測試訊息會被刪除,編譯過程以效能為重點。和效能分析模式一樣,此時只能使用實體裝置執行。雖然我們只需要在 flutter run 加入 --release 參數即可,但實務上 Flutter 支援另一個可以針對每個平台優化的指令 flutter build

在你發佈應用程式之前,我們還需要完成一些設定作業,以確保我們可以在各平台發佈。

首先,是註冊該平台的開發者例如 Google Play 和 App Store 都需要一組帳號。此時建議參考各平台的官方文件。另外在發佈 Android 應用程式的時候,注意 Google Play 並不是唯一的商店,Android 系統還有其他的商店,但只有在 Google Play 發佈你才可以使用例如 Google 地圖等服務、套件還有內購。因此本文僅介紹 Google Play 。

要在 Google Play 發佈要先到 Google Play Console 註冊,2024 的費用為一次性費用 $25。

同樣的要發佈到 App Store 需支付每年 $99 的費用 Apple Developer Program

值得一提的是兩個平台都會收取銷售費用。大致上分成 3 種類型:

  • App purchases: App 一次性費用,兩個商店都是抽 30%
  • 訂閱:採用訂閱模式向使用者收取費用模式。第一年收取 30%,保留客戶超過 12 個月則 15%。
  • 內購:在應用程式內購買行為抽 30%

基本上不允許宣傳使用替代購買路徑,注意這些 30%費用僅適用於數位化商品或服務。實體服務如 Uber 搭車不會產生此費用。不過因為一些壟斷行為的爭議。蘋果針對收入小於 100 萬美元的公司已將 30% 調降至 15%(Small Business Program)由於這些規範會隨著時間變化,因此還需要關注各平台的最新資訊。

Android

在 Android,.aab 是目前 Google Play 主流的上架格式。當我們執行 flutter build appbundle 指令時可以產出這個格式的檔案。

你可能曾經聽過 .apk 檔案,雖然已經上架過的 App 依舊支援,但新的應用程式必須使用 .aab 格式上傳。另外在我們上傳檔案之前,我們還需要確保提供的資訊都是正確的如 package name ,圖片都符合規範尺寸,版本訊息等等。

AndroidManifest 和 build.gradle

對於個別平台,Flutter 專案會有其專用目錄用於儲存該平台所需的檔案協助編譯流程。通常你可以觀察到 ios android web 等目錄。一般來說我們不需要去編輯這些目錄中的檔案,除非要為一些插件修改設定。

在 Android, 關於應用程式的一些設定資訊會在 android 目錄,也就是 app/src/main/AndroidManifest.xmlapp/build.gradle 檔案,每當要發佈之前我們會需要檢查這些設定。

權限

其中一個重要的步驟就是關於授權的請求,通常在 app/src/main/AndroidManifest.xml 檔案中。不管是基於安全性或者平台的規範我們建議只請求應用程式實際會用到需要的權限就好。如果申請超過所需的權限,應用程式可能會被進一步分析以至於下架。

在我們的範例專案中我們沒有要求任何權限。如果你加入了一些插件可能就會需要額外的權限。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hello_world">
  <uses-permission
        android:name="android.permission.INTERNET"/>
  <uses-permission
        android:name="android.permission.READ_CONTACTS" />
  <uses-permission
        android:name="android.permission.WRITE_CONTACTS"/>
  <uses-permission
        android:name="android.permission.CAMERA" />
  <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
  ...
</manifest>

應用程式所需的權限使用 uses-permission 標籤設定,而 android:name 就是權限的名稱,由 Android 所定義,通常插件的安裝教學會指示你設定必須的權限。

除了權限,還有 uses-feature 標籤,它的作用是限制安裝到具有特定功能的裝置上。例如上面的範例表示如果裝置沒有相機也可以安裝。在發佈應用程式到 Google Play 的過程中,如果權限設定會限制安裝的裝置,介面會顯示警告。

其中,普通的權限,表示在安裝時就會取得授權,而另一些權限則須在執行時期發動請求,使用者可以選擇授權與否。

Meta 標籤

另外非常重要的步驟,就是檢查加入的 Meta 標籤,這些標籤通常是為了某些服務例如 AdMob 或者 Google Map。你可能在開發時期已經設定了測試環境的資料,我們此時須確保相關資料屬於正式環境。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handson">
  ...
  <application>
    ...
    <meta-data
      android:name="com.google.android.gms.ads.APP_ID"
      android:value="ADMOB-KEY"/>
  </application>
</manifest>

應用程式名稱和圖示

直到這邊,每當我們測試應用程式,我們可以看到都是使用 Flutter 預設的 Logo。而正式版本,我們通常還是會希望使用我們自己的圖示。與此同時我們也需要為我們的應用程式命名。

在設定圖示時,有兩種方式 - 比較簡單自動化的方式和手動設定。

手動設定圖示和名稱

圖示和名稱定義在 AndroidManifest.xmlapplication 標籤

<manifest ...>
    <application
        android:label="hello_world"
        android:icon="@mipmap/ic_launcher">
     ...
    </application>
</manifest>

android:label 沒有什麼大問題,而圖示 android:icon 的部分,我們需要圖片資源,在 Android,資源檔放在 /app/src/main/res/ 目錄下。在這個目錄下有很多目錄,這些目錄分別以螢幕尺寸、版本系統等等來區分,例如 values-zh 表示中文,drawable-xxhdpi 表示某種解析度下,mipmap-* 對應各種解析度的圖示。

我們需要替換 ic_launcher.png ,具體設計規範可以參考Material Design 文件

使用插件來設定圖示

但我們設計好一張圖片,但看到那麼多尺寸我們不想花太多時間來修改尺寸。這時我們可以使用 flutter_launcher_icons 插件,當然第一步是先安裝,且注意是 dev 相依

dev_dependencies:
  flutter_launcher_icons: "^0.13.1"
  # https://github.com/fluttercommunity/flutter_launcher_icons

安裝之後我們可以參考官方文件在 pubspec.yaml 設定或者獨立出設定檔案:

$ dart run flutter_launcher_icons:generate

該指令會為我們建議相關設定檔案 - flutter_launcher_icons.yaml

flutter_launcher_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/icon.png"
  min_sdk_android: 21 # android min sdk min:16, default 21
  web:
    generate: true
    image_path: "path/to/image.png"
    background_color: "#hexcode"
    theme_color: "#hexcode"
  windows:
    generate: true
    image_path: "path/to/image.png"
    icon_size: 48 # min:48, max:256, default: 48
  macos:
    generate: true
    image_path: "path/to/image.png"

上面設定的 true設定插件對於該平台可以覆寫已經存在的圖示,image_path 設定圖片路徑。

最後我們執行指令產生各尺寸圖片

$ flutter pub run flutter_launcher_icons

Application ID 和版本

applicationId 值是應用程式在 Google Play 和 Android 系統上唯一的識別名稱。通常建議使用組織的網域反過來搭配應用程式名稱作為套件 ID

com.companyname.appname

注意這個設定因為一旦上傳之後這個值就不能變更了。

app/build.gradle 檔案中可以找到 defaultConfig 設定

android {
    namespace = "city.uspace.test_demo"
    compileSdk = flutter.compileSdkVersion
    
    // ...

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "city.uspace.test_demo"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
        minSdk = flutter.minSdkVersion
        targetSdk = flutter.targetSdkVersion
        versionCode = flutterVersionCode.toInteger()
        versionName = flutterVersionName
    }

    // ...
}

除了 applicationIdminSdkVersion 設定了最低支援的 Android API 版本。在 Flutter 中,通常有兩種狀況需要改變 minSdkVersion 的設定:

  • Flutter 框架的要求變更了
  • 如果使用的插件需要更新版本的 SDK

targetSdkVersion 設定我們預期設計運行的的 Android API 版本(針對哪個 Android 版本進行了優化和測試),用於管理 manifest 功能是可以使用的,通常會設定最新版本。

這些設定在我們的 local.properties 檔案中,如果需要變更

flutter.minSdkVersion=21
flutter.targetSdkVersion=30

versionCodeversionName 會自動從 pubspec.yaml 擷取。所以假如我們在 pubspec.yaml 設定了 version: 1.0.0+1 那麼這個值會被拆成 versionCode 為 1,versionName 為 1.0.0。

簽署應用程式

簽署是最後也是發佈之前最重要的一個步驟,就算你不想發佈到 Google Play。簽署這個動作是為了確認了應用程式的所有權 - 簡而言之,誰擁有這個應用程式。我們會需要這個來管理您的應用程式,例如發佈更新。

首先,讓我們看一下 app/build.gradle 文件中的 buildTypes 部分:

buildTypes {
    release {
        // TODO: Add your own signing config for the release build.
        // Signing with the debug keys for now, so `flutter run --release` works.
        signingConfig = signingConfigs.debug
    }
}

其中的 signingConfig 屬性為簽署設定。我們需要變更這個為我們擁有的簽名。此時我們須執行下面步驟。

1. 產生 keystore 檔案

第一步需要產生我們的開發者 keystore 檔案(你可以在多個應用程式使用同一個 keystore)。

$ keytool -genkey -v -keystore ./[檔名或自訂完整輸出路徑].jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

這把鑰匙要放置在 android/app 目錄下。

2. 建立 key.properties 檔案

android 目錄下建立 key.properties 檔案

storePassword=[建立 keystore 的密碼]
keyPassword=[建立 keystore 的密碼]
keyAlias=key
storeFile=[keysotre 檔名].jks

3. 載入 key.properties

app/build.gradle 載入我們建立的新檔案並建立一個 signingConfig 類別。在 android { 之前加入下面設定

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
	...

這個設定定義了新的 keystoreProperties 變數,然後定義了檔案的變數並載入。

4. 使用 keystoreProperties

然後我們需要使用這個 keystoreProperties 。一樣在 app/build.gradle 檔案中靠近 buildTypes 上面加入設定如下:

signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
        storePassword keystoreProperties['storePassword']
    }
}

buildTypes {
    release {
        signingConfig = signingConfigs.debug
    }
}

5. 使用新的簽署設定

最後我們就可以把 buildTypes 中的 signingConfig設定換成 signingConfigs.release

buildTypes {
  release {
    signingConfig signingConfigs.release
  }
}

現在,當我們建置 app bundle .abb 時,會使用我們的金鑰來簽署。這個步驟雖然有點複雜,但所幸我們只需要做一次。在搞定全部設定之後,我們就可以執行

$ flutter build appbundle

你可能很好奇 signingConfigs.debug 哪來的?這是由 Flutter 框架自動設定的,預設在 $HOME/.android/debug.keystore。如果想查詢配置可以使用指令 ./gradlew signingReport

建置完成後你應該可以在指令介面看到檔案輸出的路徑如 build/app/outputs/bundle/release/app-release.aab。然後到 Google Play Console 我們就可以建立新的 release 並上傳 aab 檔案。

在 Google Play Console 支援不同類型的「發佈」:

  • 內部測試:這個 app 只有特定測試者可以安裝
  • 封閉測試:類似內部測試,但對象數量會在更多一點
  • 開放測試:更大範圍的對象,且任何人可以參加開放測試
  • 正式版本:任何造訪 Play Store 都可以下載安裝

除了我們重點的應用程式之外,發佈過程還需要一些展示的圖片,螢幕截圖,描述,隱私權政策的網址,聯絡資訊等,還有一些權限說明等都需要時間,這並不是幾分鐘就能搞定的,草草亂填可能會導致 app 被拒絕上架。

iOS

發佈 iOS 應用程式的步驟會比 Android 更加複雜。雖然我們可以在自己的裝置上測試,但在 App Store 發佈的流程比起來確實稍微困難。首先我們需要一組 Apple Developer 開發者帳號。

然後我們必須使用 Xcode 提供相關設定。Xcode 是開發原生 iOS 應用程式的 IDE,一般來說只能在 macOS 上執行。我們可以從 Apple App Store 上免費下載。Flutter 會使用它來幫助我們打包應用程式以及處理發佈。

App Store Connect

Android 應用程式在我們準備好 .aab 準備發佈之前我們是不需要在 Google Play 進行設定。但在 iOS 的流程不一樣。上傳和發佈可以在 Xcode 中管理(資深開發者表示:古早時也是需要!😆),並且為了上傳應用程式,我們需要在 App Store Connect 先建立,然後在 Xcode 中建置並上傳。

1. 註冊 Bundle ID

所有的 iOS 應用程式都需要在蘋果註冊一組唯一的 Bundle ID。為了註冊 ID 我們須到 Apple Developer

  1. 選擇「Identifiers」

  2. 建立「App Id」

  3. 選擇 App 類型

  4. 輸入 Bundle Id,這個 ID 可以和 Android 的 applicationId 一樣。

    com.companyname.appname
    
  5. 加入所需的功能,完成。

2. 建立 App

接著,到 Apple Store Connect

  1. 開啟「App」頁面
  2. 點擊「+」建立新的 App
  3. 輸入相關資訊並建立
  4. 進入 App Information
  5. 選擇註冊的 Bundle ID

完成這些步驟之後,就可以回到 Xcode 進行設定。

Xcode

在 Xcode,我們須進行一些設定以完成發佈前的準備。首先,應用程式的圖示,名稱,Bundle ID。這些東西和 Android 非常類似。

使用 Xcode 開啟專案,Flutter 為我們建立了 .xcworkspace 檔案,在 ios/Runner.xcworkspace 。因此我們可以開啟這個專案。

$ open ios/Runner.xcworkspace

注意,在 ios/ 目錄下還有一個名稱非常類似的檔案 Runner.xcodeproj ,請不要使用這個檔案,使用這個檔案會導致 Xcode 無法正確設定。

Bundle ID 和應用程式設定

在「General」頁籤我們可以編輯「Display Name」也就是使用者在 App Store 看到的名稱。

「Bundle Identifier」我們使用前面設定的 Application ID ,在不同平台保持應用程式 ID 一致可以簡化維護工作。

選擇「Project」的「Runner」在「Deployment Target」下,可以設定最低 iOS 版本。如同在 Android 部分討論的,這個最低版本很可能會根據你的應用程式使用的插件而有所調整。例如,flutter_stripe 插件要求最低 iOS 版本為 13。

如果你需要更改 Deployment Target 設定,那麼你還需要更新 ios/Flutter/AppframeworkInfo.plist 檔案,並將 MinimumOSVersion 值設定為與你在 Xcode 中設定的值相匹配。

<key>MinimumOSVersion</key>
<string>12.0</string>

另外請注意 Version 和 Build 值 - 它們分別類似於 Android 中的版本名稱 versionName 和版本代碼 versionCode。每次你向 App Store 上傳應用程式的新版本時,你需要確保已經在 pubspec.yaml 文件中增加了版本值;否則,上傳的版本將會被 App Store Connect 拒絕。

App 圖示

在 Android 的部分我們學習到了使用插件自動產生圖示。雖然我們強烈推薦這種方式,但為了教學的完整,這裡我們使用手動來更新。

首先請先參考蘋果的 iOS 圖示指南,我須了解平台的限制,例如透明的圖示會被拒絕,請確保我們的圖示是完全不透明的。詳細請參考

處理好圖片之後,在 Xcode,選擇「Runnder」目錄下的 Assets.xcassets,點擊「AppIcon」 更新成我們的圖片。

簽署應用程式

類似 Android,我們需要讓平台知道應用程式的歸屬。在 iOS ,現在可以使用 Xcode 來管理,我們不需要在自己產生各種憑證、金鑰。當我們註冊開發者帳號並購買「Apple Developer Program」,基本上準備就算完成了。

我們到 Xcode 切換到 「Signing & Capabilities」頁籤,就會看到自動管理簽署的部分「Automatically manage signing」,除非我們需要例如處理 Apple Pay 這些功能,否則我們大概不需要自己管理這些簽署。

確認 「Team」和帳號登入、設定正確,後續 Xcode 就會幫我們自動處理。

建置和上傳

建置編譯的第一步是安裝 CocoaPods,這一步也是蠻容易遇到問題的一步,可能需要查看 Github 的 Issues 或者 Google 解決辦法,通常下面的指令也許可以協助你解決問題

$ pod deintegrate
$ pod repo update
$ pod install

之後就可以執行:

$ flutter build ipa

這個指令會產生 .ipa ,其類似於 Android 的 .aab。成功建置 ipa 後,我們需要使用 Xcode 開啟專案目錄下路徑檔案

build/ios/archive/[YourApp].xcarchive

然後 Xcode 會彈出視窗協助您上傳應用程式,點擊「Distribute App」即可開始上傳。一旦上傳成功,接著應用程式會須經過 Apple 會先進行一次自動審核確保設定正確,如果有任何問題您會收到相關通知和建議。約 30 分鐘後自動審核大概率會完成,然後我們就可以進行測試。iOS 的測試和 Android 有些不同,只有一個測試階段。通常測試人員須安裝 TestFlight 才能進行測試版本的安裝。

同樣的正式發佈之前也是需要設定商店一些展示的資訊和圖片,提交之後可能會需要幾天審核時間。後續你可以安裝「App Connect」應用程式來接收通知。如果長時間未過審核說明可能由問題,你可以通過網頁管理介面查詢相關資訊。

關於 PWA

除了應用程式之外,Flutter 的網頁應用預期也支援離線安裝 Progressive Web App。目前 PWA 仍在持續改進中,使用前請自行評估。

監控應用程式使用情形與異常

當我們發佈正式版本應用程式後,我們很難直接知道使用者如何操作以及異常狀態。行動應用的更新不像 Web 那樣快速,因為得經過審查,為了可以儘早知道使用狀況或者當機問題我們可以使用前面介紹過的 Firebase 工具:Crashlytics 和 Google Analytics。

尤其是 Crashlytics ,每當我們的程式發生非預期的 Crash,或者我們自己拋出例外,Crashlytics 會收到資訊並傳回伺服端,我們可以使用其介面來了解情況。尤其是一些非同步操作,例如資料庫連線或 API 讀取失敗,使用者可能不知道發生了故障,但你可以從儀表板上發現問題。除此之外,Crashlytics 也會收錄當機的應用程式版本、是否需要重新啟動、設備詳細資訊,以及當機時的堆疊。

我們簡單快速的概覽了發佈的流程和需要的工作,雖然實務上可能不會第一次就這麼順利,但大概您都還是可以在社群、網路上找到解決方案。

作者的話:感謝您對本系列的閱讀也希望能提供一些幫助,在第 30 天,這個主題也算是圓滿了整個學習計畫,但由於個人仍需要深入評估導入此框架的分析,因此,我們後續還會有些題目例如:藍芽、Rive 以及實戰上到底如何才是比較好的最佳實踐。


上一篇
Day 29 整合測試
下一篇
Flutter 藍芽通訊使用 flutter_blue_plus
系列文
Flutter 開發實戰 - 30 天逃離新手村38
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言